/*
    Copyright (C) 2021 Huawei Device Co., Ltd.
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
 */

package com.danikula.videocache.slicingStrategy;

import com.danikula.videocache.util.LogUtil;
import com.danikula.videocache.ProxyCacheException;
import com.danikula.videocache.util.ProxyCacheUtils;
import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.Files;
import com.danikula.videocache.file.UnlimitedDiskUsage;
import com.danikula.videocache.interfacers.ISliceCache;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static com.danikula.videocache.Constants.TEMP_POSTFIX;

/**
 * 分片缓存的具体实现
 */
public class SlicingFileCache extends ISliceCache {
    private SlicingCacheStrategy cacheStrategy;
    private String completeName = "";
    private String cacheVideoUrl;
    private String rootPath = "";
    private long fileCacheStart; // 当前缓存碎片文件的起始位置
    private long fileLength = Long.MAX_VALUE; // 文件总长度
    private long fileMergeStart; // 即将与当前缓存碎片文件合并的最近一个碎片文件的起始位置
    private long fileMergeEnd; // 即将与当前缓存碎片文件合并的最近一个碎片文件的终止位置
    private boolean isMergeFinish = true; // 文件合并是否结束
    private List<long[]> fileStartList;

    public SlicingFileCache(File file) throws ProxyCacheException {
        this(file, new UnlimitedDiskUsage(), new SlicingCacheStrategy());
    }

    public SlicingFileCache(File file, DiskUsage diskUsage, SlicingCacheStrategy cacheStrategy)
            throws ProxyCacheException {
        try {
            if (diskUsage == null || cacheStrategy == null) {
                throw new NullPointerException();
            }
            this.cacheStrategy = cacheStrategy;
            this.diskUsage = diskUsage;
            File directory = file.getParentFile();
            Files.makeDir(directory);
            rootPath = directory.getCanonicalPath();
            cacheStrategy.restoreFilePath(rootPath);
            boolean completed = file.exists();
            completeName = file.getCanonicalPath();
            if (completeName == null) {
                completeName = "";
            }
            this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
            this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
        } catch (IOException e) {
            LogUtil.error(this.getClass().getSimpleName(), e.toString());
        }
    }

    /**
     * 设置视频Url 用于生成碎片文件的文件夹
     *
     * @param cacheVideoUrl 视频Url
     */
    public void setCacheVideoUrl(String cacheVideoUrl) {
        this.cacheVideoUrl = cacheVideoUrl;
    }

    /**
     * 根据视频拖动位置来获取当前缓存碎片文件(新创建或者使用已有的)
     * 用于写入和读取数据
     *
     * @param cacheOffset 视频拖动位置
     */
    public synchronized void setCacheOffset(long cacheOffset) {
        try {
            fileLength = cacheStrategy.getFileLength();
            if (this.file != null && isCompleted()) {
                fileCacheStart = 0;
                return;
            }
            // 只有每次起始点变动 上次的碎片可能仅仅只下载一点点 需要删除很小的碎片文件
            // 不变动会慢慢的下载完直至合并
            SlicingFileUtil.deleteFile(cacheVideoUrl, rootPath, cacheStrategy.getMinSingleFileSize(),
                    cacheStrategy.getMinFileCacheSize(), fileLength);
            Optional<File> optional = SlicingFileUtil.getCurrentFile(cacheOffset, cacheVideoUrl, rootPath,
                    cacheStrategy.getMinFileCacheSize(), fileLength);
            if (optional == null || !optional.isPresent() || optional.get() == null) {
                close();
                return;
            }
            File currentFile = optional.get();
            if (currentFile != this.file) {
                fileCacheStart = SlicingFileUtil.getFileCacheStart(currentFile);
                this.file = currentFile;
                this.dataFile = new RandomAccessFile(this.file, "rw");
            }
        } catch (IOException | ProxyCacheException e) {
            LogUtil.error(this.getClass().getSimpleName(), e.toString());
        }
    }

    /**
     * 根据网络请求获取的视频总时长设置文件长度
     *
     * @param fileTotalLength 络请求获取的视频总时长
     */
    public void setFileLength(long fileTotalLength) {
        if (fileTotalLength <= 0) {
            return;
        }
        this.fileLength = fileTotalLength;
        cacheStrategy.restoreFileLength(fileTotalLength);
        if (fileMergeStart == 0 || fileMergeStart == Long.MAX_VALUE) {
            fileMergeStart = fileTotalLength;
        }
        if (fileMergeEnd == 0 || fileMergeEnd == Long.MAX_VALUE) {
            fileMergeEnd = fileTotalLength;
        }
    }

    @Override
    public synchronized long available() throws ProxyCacheException {
        try {
            return (int) dataFile.length();
        } catch (IOException e) {
            throw new ProxyCacheException("Error reading length of file " + dataFile, e);
        }
    }

    @Override
    public synchronized void complete() throws ProxyCacheException {
        if (!isCompleted()) {
            return;
        }
        close();
        File completedFile = new File(completeName);
        File tempDownFile = new File(completeName + TEMP_POSTFIX);
        if (completedFile.exists()) {
            boolean delete = completedFile.delete();
            LogUtil.info("complete", "completedFile delete is success: " + delete);
        }
        if (tempDownFile.exists()) {
            boolean delete = tempDownFile.delete();
            LogUtil.info("complete", "tempDownFile delete is success: " + delete);
        }
        boolean renamed = file.renameTo(completedFile);
        if (!renamed) {
            throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!");
        }
        try {
            file = completedFile;
            cacheStrategy.restoreCompleteFilePath(file.getCanonicalPath());
            dataFile = new RandomAccessFile(file, "r");
            diskUsage.touch(file);
        } catch (IOException e) {
            throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
        }
    }

    @Override
    public synchronized boolean isCompleted() throws ProxyCacheException {
        try {
            if (fileLength > 0 && fileLength != Long.MAX_VALUE) {
                if (available() == fileLength) {
                    return true;
                }
            }
        } catch (ProxyCacheException e) {
            String format = "get available wrong ";
            throw new ProxyCacheException(format, e);
        }
        return false;
    }

    /**
     * Returns file to be used fo caching. It may as original file passed in constructor as some temp file for not
     * completed cache.
     *
     * @return file for caching.
     */
    public File getFile() {
        return file;
    }


    @Override
    public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
        synchronized (dataFile) {
            return beginRead(buffer, offset, length);
        }
    }


    @Override
    public int readFile(byte[] buffer, long offset, int length) throws ProxyCacheException {
        synchronized (dataFile) {
            // 读取缓存 起点为总起点减去当前碎片文件的起始位置
            // 使用synchronized是为了防止写入线程和读取线程dataFile.seek的时候指针错乱导致数据错乱
            long newOffset = offset - fileCacheStart;
            if (newOffset < 0) {
                newOffset = 0;
            }
            return beginRead(buffer, newOffset, length);
        }
    }

    private int beginRead(byte[] buffer, long offset, int length) throws ProxyCacheException {
        try {
            dataFile.seek(offset);
            return dataFile.read(buffer, 0, length);
        } catch (IOException e) {
            String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";
            throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);
        }
    }

    @Override
    public void writeFile(byte[] data, long offset, int length) throws ProxyCacheException {
        synchronized (dataFile) {
            try {
                if (isCompleted()) {
                    throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!");
                }
                // 读取缓存 直接在文件最后拼接即可
                // 使用synchronized是为了防止写入线程和读取线程dataFile.seek的时候指针错乱导致数据错乱
                dataFile.seek(offset - fileCacheStart);
                dataFile.write(data, 0, length);

            } catch (IOException e) {
                String format = "Error writing %d bytes to %s from buffer with size %d";
                throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e);
            }
        }
    }

    @Override
    public boolean isCacheEnough(long offset, int length) {
        try {
            if (!isTempFile(file)) {
                return true;
            }
            long available = available();
            // 读取到文件结尾了
            if (offset >= fileLength && fileLength > 0 && fileLength != Long.MAX_VALUE) {
                return true;
            }
            if (fileCacheStart + available == fileLength && fileLength > 0 && fileLength != Long.MAX_VALUE) {
                return true;
            }
            if (offset + length > fileLength && offset < fileLength) {
                return false;
            }
            return offset + length <= available + fileCacheStart;
        } catch (ProxyCacheException e) {
            LogUtil.error(this.getClass().getSimpleName(), e.toString());
        }
        return false;
    }

    @Override
    public long getDownLoadStart() throws ProxyCacheException {
        long available = available();
        return fileCacheStart + available;
    }

    @Override
    public long getMergeStart() throws ProxyCacheException {
        return fileMergeStart;
    }

    @Override
    public long getMergeEnd() throws ProxyCacheException {
        return fileMergeEnd;
    }


    @Override
    public void mergeFiles() throws ProxyCacheException {
        RandomAccessFile tempFile = null;
        try {
            if (fileMergeStart <= 0) {
                return;
            }
            long endOffset = fileCacheStart + available();
            if (endOffset < fileMergeStart) {
                return;
            }
            Optional<File> optional = SlicingFileUtil.getMergeFile(cacheVideoUrl, rootPath, fileMergeStart);
            if (optional == null || !optional.isPresent() || optional.get() == null) {
                return;
            }
            File mergeFile = optional.get();
            long mergeStart = endOffset - fileMergeStart;
            tempFile = new RandomAccessFile(mergeFile, "rw");
            byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
            int readBytes;
            while ((readBytes = tempFile.read(buffer)) != -1) {
                dataFile.seek(available() - mergeStart);
                dataFile.write(buffer, 0, readBytes);
                mergeStart = 0;
            }
            boolean delete = mergeFile.delete();
            isMergeFinish = true;
            LogUtil.info(this.getClass().getSimpleName(), "mergeFiles delete old file is success: " + delete);
        } catch (IOException e) {
            LogUtil.error(this.getClass().getSimpleName(), e.toString());
        } finally {
            try {
                if (tempFile != null) {
                    tempFile.close();
                }
            } catch (IOException e) {
                LogUtil.error(this.getClass().getSimpleName(), e.toString());
            }
        }
    }

    @Override
    public void refreshMergeInfo(long offset) {
        fileStartList = SlicingFileUtil.getFileStartList(cacheVideoUrl, rootPath);
        fileMergeStart = 0;
        fileMergeEnd = 0;
        for (int i = 0; i < fileStartList.size(); i++) {
            long[] longs = fileStartList.get(i);
            if (offset < longs[0]) {
                fileMergeStart = longs[0];
                fileMergeEnd = longs[1];
                break;
            }
        }
        if (fileMergeStart == 0) {
            if (fileLength > 0) {
                fileMergeStart = fileLength;
            } else {
                fileMergeStart = Long.MAX_VALUE;
            }
        }
        if (fileMergeEnd == 0) {
            if (fileLength > 0) {
                fileMergeEnd = fileLength;
            } else {
                fileMergeEnd = Long.MAX_VALUE;
            }
        }
    }

    @Override
    public long getTotalCache() {
        return SlicingFileUtil.getCacheFileSize(cacheVideoUrl, rootPath);
    }

    @Override
    public List<long[]> getAllCacheArea() {
        if (isTempFile(file)) {
            long fileStart = SlicingFileUtil.getFileCacheStart(file);
            if (fileStartList == null) {
                fileStartList = SlicingFileUtil.getFileStartList(cacheVideoUrl, rootPath);
            }
            int count = 0;
            for (int i = 0; i < fileStartList.size(); i++) {
                long[] longs = fileStartList.get(i);
                long[] temp = new long[2];
                if (longs[0] == fileStart) {
                    count++;
                    temp[0] = longs[0];
                    temp[1] = longs[0] + file.length();
                    fileStartList.set(i, temp);
                    break;
                }
            }
            if (count == 0) {
                long[] temp = new long[2];
                temp[0] = fileStart;
                temp[1] = fileStart + file.length();
                fileStartList.add(temp);
            }
            return fileStartList;
        } else {
            List<long[]> list = new ArrayList<>();
            list.add(new long[]{0, file.length()});
            return list;
        }
    }

    public boolean isMergeFinish() {
        return isMergeFinish;
    }

    public void setMergeFinish(boolean mergeFinish) {
        isMergeFinish = mergeFinish;
    }

    public long getFileLength() {
        return fileLength;
    }
}

